// SPDX-FileCopyrightText: 2018 - 2022 UnionTech Software Technology Co., Ltd.
// 
// SPDX-License-Identifier: GPL-3.0-or-later
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/printer"
	"go/token"
	"io/ioutil"
	"log"
	"os"
	"strings"
	"unicode"

	"github.com/linuxdeepin/go-lib/strv"
)

func astNodeToStr(fSet *token.FileSet, node interface{}) (string, error) {
	var buf bytes.Buffer
	err := printer.Fprint(&buf, fSet, node)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

type Property struct {
	Name  string
	Type  string
	Equal string
}

type Generator struct {
	buf bytes.Buffer
	pkg Package
}

func (g *Generator) printf(format string, args ...interface{}) {
	_, err := fmt.Fprintf(&g.buf, format, args...)
	if err != nil {
		log.Fatal(err)
	}
}

const propsMuField = "PropsMu"

func (g *Generator) genHeader() {
	cmdline := strings.Join(os.Args, " ")
	g.printf("// Code generated by %q; DO NOT EDIT.\n\n", cmdline)
	g.printf("package %s\n", g.pkg.name)

	if len(g.pkg.extraImports) > 0 {
		g.printf("import (\n")

		for _, imp := range g.pkg.extraImports {
			g.printf(imp + "\n")
		}
		// end import
		g.printf(")\n\n")
	}
}

func (g *Generator) genSetProps(files []string, types strv.Strv) {
	g.parseFiles(files, types)

	for typ, props := range g.pkg.typePropMap {
		for _, prop := range props {
			// set property method
			returnVarType := "(changed bool)"
			if prop.Equal == "nil" {
				returnVarType = ""
			}
			g.printf("func (v *%s) setProp%s(value %s) %s {\n",
				typ, prop.Name, prop.Type, returnVarType)

			switch prop.Equal {
			case "nil":
				g.printf("v.%s = value\n", prop.Name)
				g.printf("v.emitPropChanged%s(value)\n", prop.Name)
			case "":
				g.printf("if v.%s != value {\n", prop.Name)
				g.printf("    v.%s = value\n", prop.Name)
				g.printf("    v.emitPropChanged%s(value)\n", prop.Name)
				g.printf("    return true\n")
				g.printf("}\n")
				g.printf("return false\n")
			default:
				expr := fmt.Sprintf("%s(v.%s, value)", prop.Equal, prop.Name)
				if strings.HasPrefix(prop.Equal, "method:") {
					method := prop.Equal[len("method:"):]
					expr = fmt.Sprintf("v.%s.%s(value)", prop.Name, method)
				}

				g.printf("if !%s {\n", expr)
				g.printf("    v.%s = value\n", prop.Name)
				g.printf("    v.emitPropChanged%s(value)\n", prop.Name)
				g.printf("    return true\n")
				g.printf("}\n")
				g.printf("return false\n")
			}
			g.printf("}\n\n")

			// method emitPropChangedXXX
			g.printf("func(v *%s) emitPropChanged%s(value %s) error {\n",
				typ, prop.Name, prop.Type)
			g.printf("    return v.service.EmitPropertyChanged(v, \"%s\", value)\n",
				prop.Name)
			g.printf("}\n\n")
		}
	}
}

type Package struct {
	name        string
	typePropMap map[string][]Property
	//              ^type name
	extraImports []string
}

func (g *Generator) parseFiles(names []string, types strv.Strv) {
	log.Printf("parseFiles names: %v, types: %v\n", names, types)
	fs := token.NewFileSet()
	var typePropMap = make(map[string][]Property)

	for _, name := range names {
		if !strings.HasSuffix(name, ".go") {
			//not go file
			continue
		}

		f, err := parser.ParseFile(fs, name, nil, parser.ParseComments)
		if err != nil {
			log.Fatalf("failed to parse file %q: %s", name, err)
		}
		ast.Inspect(f, func(node ast.Node) bool {
			decl, ok := node.(*ast.GenDecl)

			if !ok || decl.Tok != token.TYPE {
				return true
			}

			for _, spec := range decl.Specs {
				typeSpec, ok := spec.(*ast.TypeSpec)
				if !ok {
					continue
				}

				structType, ok := typeSpec.Type.(*ast.StructType)
				if !ok {
					continue
				}

				if !types.Contains(typeSpec.Name.Name) {
					continue
				}

				props := getProps(fs, structType)
				typePropMap[typeSpec.Name.Name] = props
			}
			return true
		})

	}

	g.pkg.typePropMap = typePropMap

}

func (g *Generator) format() []byte {
	src, err := format.Source(g.buf.Bytes())
	if err != nil {
		log.Println("warning: internal error: invalid Go generated:", err)
		return g.buf.Bytes()
	}
	return src
}

var (
	_typeNames    string
	_extraImports string
	_outputFile   string
)

var defaultFlagSet = flag.NewFlagSet("default", flag.PanicOnError)

var emFlagSet = flag.NewFlagSet("em", flag.PanicOnError)

func init() {
	defaultFlagSet.StringVar(&_typeNames, "type", "", "comma-separated list of type names; must be set")
	defaultFlagSet.StringVar(&_extraImports, "import", "", "")
	defaultFlagSet.StringVar(&_outputFile, "output", "", "output file")

	emFlagSet.StringVar(&_typeNames, "type", "", "comma-separated list of type names; must be set")
	emFlagSet.StringVar(&_extraImports, "import", "", "")
	emFlagSet.StringVar(&_outputFile, "output", "", "output file")
}

func main() {
	log.SetFlags(log.Lshortfile)
	log.SetPrefix("dbusutilv1-gen: ")

	modeExportedMethods := false
	if len(os.Args) > 1 && os.Args[1] == "em" {
		modeExportedMethods = true
		err := emFlagSet.Parse(os.Args[2:])
		if err != nil {
			log.Fatal(err)
		}
	} else {
		err := defaultFlagSet.Parse(os.Args[1:])
		if err != nil {
			log.Fatal(err)
		}
	}
	types := strv.Strv(strings.Split(_typeNames, ","))
	var parsedExtraImports []string
	for _, imp := range strings.Split(_extraImports, ",") {
		if imp == "" {
			continue
		}
		if strings.Contains(imp, "=") {
			parts := strings.SplitN(imp, "=", 2)
			pkg := parts[0]
			alias := parts[1]
			// github.com/godbus/dbus/v5=dbus,bytes
			parsedExtraImports = append(parsedExtraImports, fmt.Sprintf("%s \"%s\"",
				alias, pkg))
		} else {
			parsedExtraImports = append(parsedExtraImports, `"`+imp+`"`)
		}
	}
	goPackage := os.Getenv("GOPACKAGE")
	if goPackage == "" {
		log.Fatal("env GOPACKAGE is empty")
	}
	g := &Generator{
		pkg: Package{
			name:         goPackage,
			extraImports: parsedExtraImports,
		},
	}
	if modeExportedMethods {
		if _outputFile == "" {
			_outputFile = "exported_methods_auto.go"
		}
		g.pkg.extraImports = append(g.pkg.extraImports, `"github.com/linuxdeepin/go-lib/dbusutilv1"`)
		g.genHeader()
		g.genExportedMethods(types)
	} else {
		files := defaultFlagSet.Args()
		if _outputFile == "" {
			_outputFile = g.pkg.name + "_dbusutil.go"
		}
		g.genHeader()
		g.genSetProps(files, types)
	}
	log.Println("output file:", _outputFile)
	data := g.format()
	err := ioutil.WriteFile(_outputFile, data, 0644) //#nosec G306
	if err != nil {
		log.Fatal(err)
	}
}

func isExportField(fieldName string) bool {
	return unicode.IsUpper(rune(fieldName[0]))
}

func getProps(fs *token.FileSet, structType *ast.StructType) []Property {
	//ast.Print(fs, structType)

	var prevField *ast.Field
	var props []Property
	for _, field := range structType.Fields.List {
		if len(field.Names) != 1 {
			prevField = field
			continue
		}

		fieldName := field.Names[0].Name
		if !isExportField(fieldName) {
			prevField = field
			continue
		}
		if fieldName == propsMuField {
			prevField = field
			continue
		}

		var equal string
		if field.Doc != nil {
			comments := field.Doc.List
			if len(comments) > 0 {
				option := getOptionFromComment(comments[0].Text)
				if option != "" {
					log.Printf("field %s option %s", fieldName, option)
				}

				if option == "ignore" {
					prevField = field
					continue

				} else if option == "ignore-below" {
					break
				}

				equal = getEqualFunc(option)
			}
		}

		fieldType, err := astNodeToStr(fs, field.Type)
		if err != nil {
			log.Fatal(err)
		}

		if fieldType == "sync.RWMutex" && prevField != nil &&
			len(prevField.Names) == 1 {
			prevFieldName := prevField.Names[0].Name
			if prevFieldName+"Mu" == fieldName {
				// ignore this field and prev field
				props = props[:len(props)-1]
				prevField = field
				continue
			}
		}

		props = append(props, Property{
			Name:  fieldName,
			Type:  fieldType,
			Equal: equal,
		})

		prevField = field
	}
	return props
}

func getEqualFunc(option string) string {
	idx := strings.Index(option, "equal=")
	if idx != -1 {
		equal := option[idx+len("equal="):]
		return strings.TrimSpace(equal)
	}

	return ""
}

func getOptionFromComment(comment string) string {
	comment = strings.TrimPrefix(comment, "//")
	comment = strings.TrimSpace(comment)

	const prefix = "dbusutilv1-gen:"
	if !strings.HasPrefix(comment, prefix) {
		return ""
	}
	option := comment[len(prefix):]
	return strings.TrimSpace(option)
}
